<?php

namespace jshop\catalog\classes;

use jshop\catalog\exceptions\EsException;
use Elasticsearch\ClientBuilder;

/**
 * Class ElasticSearchOperator
 * @package jshop\catalog\classes
 */
class ElasticSearchOperator
{

    /**
     * @var
     */
    protected $index;

    /**
     * @var
     */
    protected $type;

    /**
     * @var
     */
    protected $id;

    /**
     * @var array
     */
    protected $body;

    /**
     * @var array
     */
    protected $must = [];

    /**
     * @var array
     */
    protected $filter = [];

    /**
     * @var array
     */
    protected $mustNot = [];

    /**
     * @var
     */
    protected $sort;

    /**
     * @var
     */
    protected $from;

    /**
     * @var
     */
    protected $size;

    /**
     * @var
     */
    protected $total;

    /**
     * @var
     */
    protected $items;

    /**
     * @var
     */
    protected $range;

    /**
     * @var
     */
    protected $match;

    /**
     * @var
     */
    protected $source;

    /**
     * @var
     */
    protected $scroll;

    /**
     * @var
     */
    protected $groupBy;

    /**
     * @var
     */
    protected $groupByTitle;

    /**
     * @var
     */
    protected $calculate;

    /**
     * @var
     */
    protected $calculateTitle;

    /**
     * @var string
     */
    protected $operation;

    /**
     * @var
     */
    protected $params;

    /**
     * @var
     */
    protected $result;

    /**
     * @var
     */
    protected $lang;

    /**
     * @var mixed
     */
    protected $es;

    /**
     * @var
     */
    protected $aggs;

    /**
     * elasticSearchOperation constructor.
     * @param $operation
     * @param $body
     */
    public function __construct($operation = '', $body = [])
    {
        $this->es        = $this->getEsInstance();
        $this->operation = $operation;

        $this->body = $body;
    }

    /**
     * 创建ES类的实例
     * @param null $host
     * @return \Elasticsearch\Client
     */
    private function getEsInstance($host = null)
    {
        if (!$host) {
            $host = config('app.elasticsearch.hosts', ['localhost:9200']);
        }
        return ClientBuilder::create()->setHosts($host)->build();
    }

    /**
     * @param $operator
     * @param $arguments
     * @return $this
     * @throws EsException
     */
    public function __call($operator, $arguments)
    {
        $this->setOperation($operator)->exec();

        return $this;
    }

    // region --扩展
    /**
     * @return $this
     */
    public function indices()
    {
        $this->es = $this->es->indices();

        return $this;
    }

    /**
     * @return $this
     */
    public function cluster()
    {
        $this->es = $this->es->cluster();

        return $this;
    }

    /**
     * @return $this
     */
    public function nodes()
    {
        $this->es = $this->es->nodes();

        return $this;
    }

    /**
     * @return $this
     */
    public function snapshot()
    {
        $this->es = $this->es->snapshot();

        return $this;
    }

    /**
     * @return $this
     */
    public function cat()
    {
        $this->es = $this->es->cat();

        return $this;
    }

    /**
     * @return $this
     */
    public function ingest()
    {
        $this->es = $this->es->ingest();

        return $this;
    }

    /**
     * @return $this
     */
    public function tasks()
    {
        $this->es = $this->es->tasks();

        return $this;
    }

    /**
     * @return $this
     */
    public function remote()
    {
        $this->es = $this->es->remote();

        return $this;
    }
    // endregion

    /**
     * @param $size
     * @return $this
     */
    public function size($size)
    {
        $this->size = $size;

        return $this;
    }

    /**
     * @param mixed $calculate
     */
    public function setCalculate($calculate)
    {
        $this->calculate = $calculate;
    }

    /**
     * 设置语言
     * @param $lang
     * @return $this
     */
    public function setLang($lang)
    {
        $this->lang = $lang;

        return $this;
    }

    /**
     * 选择查询字段
     * @param $source
     * @return $this
     */
    public function select($source)
    {
        $this->source = $source;

        return $this;
    }

    /**
     * @return mixed
     * @throws EsException
     */
    public function getResult()
    {
        if ( !$this->result ) {
            throw new EsException('还没有返回的数据');
        }

        return $this->result;
    }

    /**
     * @return mixed
     * @throws EsException
     */
    public function getParams()
    {
        $this->buildParams();

        return $this->params;
    }

    /**
     * @return mixed
     * @throws EsException
     */
    public function getBody()
    {
        $this->buildParams();

        return $this->params['body'];
    }

    /**
     * @param $result
     * @return $this
     */
    public function setResult($result)
    {
        $this->result = $result;

        return $this;
    }

    /**
     * @param $aggs
     * @return $this
     */
    public function setAggs($aggs)
    {
        $this->aggs = $aggs;

        return $this;
    }

    /**
     * @param $index
     * @return $this
     */
    public function setIndex($index)
    {
        $this->index = $index;

        return $this;
    }

    /**
     * @param $type
     * @return $this
     */
    public function setType($type)
    {
        $this->type = $type;

        return $this;
    }

    /**
     * @param array $must
     * @return $this
     */
    public function setMust(array $must)
    {
        if ( !empty($must) ) {
            $this->must[] = $must;
        }

        return $this;
    }

    /**
     * @param mixed $groupBy
     */
    public function setGroupBy($groupBy)
    {
        $this->groupBy = $groupBy;
    }

    /**
     * @param array $mustNot
     * @return $this
     */
    public function setMustNot(array $mustNot)
    {
        if ( !empty($mustNot) ) {
            $this->mustNot[] = $mustNot;
        }

        return $this;
    }

    /**
     * @param array $filter
     * @return $this
     */
    public function setFilter(array $filter)
    {
        if ( !empty($filter) ) {
            $this->filter[] = $filter;
        }

        return $this;
    }

    /**
     * @param $field
     * @param $order
     * @return $this
     */
    public function orderBy($field, $order = 'asc')
    {
        $this->sort = [
            $field => ['order' => $order],
        ];

        return $this;
    }

    /**
     * @param $id
     * @return $this
     */
    public function setId($id)
    {
        $this->id     = $id;
        $params['id'] = $this->id;

        return $this;
    }

    /**
     * @param $body
     * @param null $field
     * @return $this
     */
    public function setBody($body, $field = null)
    {
        if ( !$field ) {
            $this->body = $body;
        } else {
            $this->body[$field] = $body;
        }

        if ( $this->sort ) {
            $this->body['sort'] = $this->sort;
        }
        $this->params['body'] = $this->body;

        return $this;
    }

    /**
     * @param $operation
     * @return $this
     */
    public function setOperation($operation)
    {
        $this->operation = $operation;

        return $this;
    }

    /**
     * 构建嵌套查询语句
     * @param mixed ...$children
     */
    private function addChildForGroupBy(...$children)
    {
        $tmp  = [];
        $test = $this->groupBy['query']['nested']['aggs'][$this->groupByTitle] ?? [];
        foreach ($children as $child) {
            $groupBy = $this->groupBy['query'];

            $aggs = $child['query'];

            if ( $this->groupBy['is_nested'] ) {
                $this->groupBy['query']['nested']['aggs'][$this->groupByTitle . '_' . $child['key']]         = $test;
                $this->groupBy['query']['nested']['aggs'][$this->groupByTitle . '_' . $child['key']]['aggs'] = $aggs;
                $this->setAggs($this->groupBy['query']);
            } else {

                $groupBy[$this->groupByTitle]['aggs'] = $aggs;

                $tmp[$this->groupByTitle . '_' . $child['key']] = $groupBy[$this->groupByTitle];
                $this->setAggs($tmp);
            }
        }


    }

    /**
     * @return $this
     * @throws EsException
     */
    public function buildParams()
    {
        $this->buildAggs();
        if ( !$this->index || !$this->type ) {
            throw new EsException('missing index or type');
        }

        $index = $this->lang ? $this->index . '-' . $this->lang : $this->index;

        $this->params['index'] = $index;
        $this->params['type']  = $this->type;

        if ( $this->mustNot || $this->must || $this->filter ) {
            $this->boolQuery();
        }

        if ( $this->body ) {
            $this->params['body'] = $this->body;
        }

        if ( $this->source ) {
            $this->params['_source'] = $this->source;
        }

        if ( $this->aggs ) {
            $this->params['body']['aggs'] = $this->aggs;
        }

        if ( $this->size ) {
            $this->params['size'] = $this->size;
        }

        return $this;
    }

    /**
     * 执行操作
     * @param $operator
     * @return $this
     * @throws EsException
     */
    public function exec($operator = null)
    {
        $this->buildParams();
        $operator && $this->setOperation($operator);
        $this->setResult(call_user_func([$this->es, $this->operation], $this->params));

        return $this;
    }

    /**
     * 搜索操作
     * @return mixed
     * @throws EsException
     */
    public function get()
    {
        $this->setOperation('search')->exec();

        return $this->getResult();
    }

    /**
     * @return \Illuminate\Support\Collection|\October\Rain\Support\Collection
     * @throws EsException
     */
    public function getSource()
    {
        $this->get();

        $result = $this->getResult();

        return collect($result['hits']['hits'])->pluck('_source');
    }

    /**
     * @return mixed
     * @throws EsException
     */
    public function getTotal()
    {
        $this->get();

        $result = $this->getResult();

        return $result['hits']['total'];
    }

    /**
     * @return mixed
     * @throws EsException
     */
    public function getAggregations()
    {
        $this->get();

        $result = $this->getResult();

        return $result['aggregations'];
    }

    /**
     * 根据唯一键获取数据
     * @param $id
     * @param string $key
     * @return array
     * @throws EsException
     */
    public function find($id, $key = 'id')
    {
        $result = $this->whereMatch([$key => $id])->get();
        $data   = $result['hits']['total'] > 0 ? $result['hits']['hits'][0]['_source'] : [];

        return $data;
    }

    /**
     * 设置查询条件
     *
     * @param $condition array Example:['id'=>119]
     * @return $this
     */
    public function whereMatch($condition)
    {
        $body = [
            "query" => [
                "match" => $condition,
            ],
        ];
        $this->setBody($body);

        return $this;
    }

    /**
     * @param $condition
     * @return $this
     */
    public function filter($condition)
    {
        foreach ($condition as $item) {
            $this->setFilter($this->buildCondition($item));
        }

        return $this;
    }

    /**
     * @param $condition
     * @return $this
     */
    public function whereAnd($condition)
    {
        if ( empty($condition) || !$condition ) {
            return $this;
        }

        foreach ($condition as $key => $item) {
            $this->setMust($this->buildCondition([$key => $item]));
        }

        return $this;
    }

    /**
     * @param $condition
     * @return $this
     */
    public function whereNot($condition)
    {
        foreach ($condition as $item) {
            $this->setMustNot($this->buildCondition($item));
        }

        return $this;
    }

    /**
     * @param $condition
     * @return array
     */
    public function buildCondition($condition)
    {
        if ( empty($condition) ) {
            return [];
        }

        $key = key($condition);
        if ( !is_array($condition[$key]) ) {
            $condition[$key] = [$condition[$key]];
        }

        if ( !strpos($key, '.') ) {
            return ['terms' => $condition];
        }

        $path = array_first(explode('.', $key));

        $query = [
            'nested' => [
                'path'  => $path,
                'query' => [
                    'bool' => [
                        'must' => [
                            ['terms' => $condition],
                        ],
                    ],
                ],
            ],
        ];

        return $query;
    }

    /**
     * @param $field
     * @param $start
     * @param $end
     * @return $this
     */
    public function between($field, $start, $end)
    {
        $query = [
            'range' => [
                $field => [
                    'gt' => $start,
                    'lt' => $end,
                ],
            ],
        ];
        $this->setMust($query);

        return $this;
    }

    /**
     * @param $field
     * @param $start
     * @param $end
     * @return $this
     */
    public function betweenNot($field, $start, $end)
    {
        $query = [
            'range' => [
                $field => [
                    'gt' => $start,
                    'lt' => $end,
                ],
            ],
        ];
        $this->setMustNot($query);

        return $this;
    }

    /**
     * @param $field
     * @return $this
     */
    public function groupBy($field)
    {
        $this->buildGroupBy($field);

        return $this;
    }

    /**
     * @param $field
     * @return $this
     */
    public function buildGroupBy($field)
    {
        $this->groupByTitle = 'group_by_' . $field;

        if ( !strpos($field, '.') ) {
            $query    = [
                $this->groupByTitle => [
                    'terms' => [
                        'field' => $field,
                    ],
                ],
            ];
            $isNested = false;
        } else {
            $path     = array_first(explode('.', $field));
            $query    = [
                'nested' => [
                    'nested' => [
                        'path' => $path,
                    ],
                    'aggs'   => [
                        $this->groupByTitle => [
                            'terms' => [
                                'field' => $field,
                            ],
                        ],
                    ],

                ],
            ];
            $isNested = true;
        }

        $groupBy['query']     = $query;
        $groupBy['is_nested'] = $isNested;

        $this->setGroupBy($groupBy);

        return $this;
    }

    /**
     * @return array
     */
    public function buildTopHits()
    {
        return [
            'query' => [
                'data' => [
                    'top_hits' => [
                        'size' => $this->size ?? 10,
                    ],
                ],
            ],
            'key'   => 'hits',
        ];
    }

    /**
     * 聚合
     * @param $field string 字段
     * @param $operator string 操作 avg sum mix max
     * @param $filter array
     * @return $this
     */
    public function calculate($field, $operator, $filter = null)
    {
        $this->calculateTitle = array_last(explode('.', $field)) . '_' . $operator;
        $path = array_first(explode('.', $field));
        $aggs = [
            $this->calculateTitle => [
                $operator => [
                    'field' => $field,
                ],
            ],
        ];

        $nested = [
            'nested' => [
                'nested' => [
                    'path' => $path,
                ]
            ],
        ];

        if ( !strpos($field, '.') ) {
            $query    = $aggs;
            $isNested = false;
        } else {
            if ( $filter ) {
                $filterValues = array_first($filter);
                if ( !is_array($filterValues) ) {
                    $filterValues = [$filterValues];
                }
                $nested['nested']['aggs'] = [
                    'filter'.'_'.key($filter) => [
                        'filter' => [
                            'terms' => [
                                key($filter) => $filterValues
                            ]
                        ],
                        'aggs' => $aggs
                    ]
                ];
            }else{
                $nested['nested']['aggs'] = $aggs;
            }

            $query = $nested;
            $isNested = true;
        }


        $calculate['query']     = $query;
        $calculate['key']       = $this->calculateTitle;
        $calculate['is_nested'] = $isNested;

        $this->setCalculate($calculate);

        return $this;
    }

    /**
     * 根据不同情况获取计算的query
     * @return mixed
     */
    private function getCalculateAggs()
    {
        if ( $this->calculate['is_nested'] && $this->groupBy['is_nested'] ) {
            $calculate['query'] = $this->calculate['query']['nested']['aggs'];
            $calculate['key']   = $this->calculate['key'];

            return $calculate;
        }

        return $this->calculate;
    }

    /**
     * 构建聚合语句
     */
    private function buildAggs()
    {
        if ( $this->groupBy['query'] && !$this->calculate ) {
            $this->addChildForGroupBy($this->buildTopHits());
        } elseif ( !$this->groupBy['query'] && $this->calculate ) {
            $this->setAggs($this->calculate['query']);
        } elseif ( $this->groupBy['query'] && $this->calculate ) {
            $this->addChildForGroupBy($this->buildTopHits(), $this->getCalculateAggs());
        }
    }

    /**
     * 写入数据
     * @param $data
     * @return $this
     * @throws EsException
     */
    public function save($data)
    {
        $this->setBody($data);
        $this->setResult($this->setOperation('index')->exec());

        return $this;
    }

    /**
     * 布尔查询
     * @return $this
     */
    public function boolQuery()
    {
        $body = [
            'query' => [
                'bool' => [
                    'must'     => $this->must,
                    'filter'   => $this->filter,
                    'must_not' => $this->mustNot,
                ],
            ],
        ];
        $this->setBody($body);

        return $this;
    }

    /**
     * 插入数据时，处理null。
     * @return $this
     */
    public function replaceNUll()
    {
        //插入数据时，处理null。

        return $this;
    }

}